Scala练手 2048

[Scala] 2048小游戏

很长一段时间没有用过Scala,重新学习了下顺便写个小游戏练练手

在这里插入图片描述

规则

  • 相同数字的两个格子,相撞时数字会相加。
  • 每次滑动时,空白处会随机刷新出一个数字的格子。
  • 当界面全部被数字填满时游戏结束;当界面中最大数字是2048时,游戏胜利

代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
object Game2048 {
val target = 2048
var highest = 0

def main(args: Array[String]): Unit = {
SwingUtilities.invokeLater(() => {
val f = new JFrame
f.setDefaultCloseOperation(3)
f.setTitle("2048")
f.add(new Game, BorderLayout.CENTER)
f.pack()
f.setLocationRelativeTo(null)
f.setVisible(true)
})
}

class Game extends JPanel {
private val (rand, side) = (new Random, 4)
private var (tiles, game) = (Array.ofDim[Tile](side, side), State.start)

final private val colorTable =
Seq(new Color(0x701710), new Color(0xFFE4C3), new Color(0xfff4d3), new Color(0xffdac3), new Color(0xe7b08e), new Color(0xe7bf8e),
new Color(0xffc4c3), new Color(0xE7948e), new Color(0xbe7e56), new Color(0xbe5e56), new Color(0x9c3931), new Color(0x701710))

setPreferredSize(new Dimension(900, 700))
setBackground(new Color(0xFAF8EF))
setFont(new Font("SansSerif", Font.BOLD, 48))
setFocusable(true)
addMouseListener(new MouseAdapter() {
override def mousePressed(e: MouseEvent): Unit = {
startGame()
repaint()
}
})
addKeyListener(new KeyAdapter() {
override def keyPressed(e: KeyEvent): Unit = {
e.getKeyCode match {
case KeyEvent.VK_UP => moveUp()
case KeyEvent.VK_DOWN => moveDown()
case KeyEvent.VK_LEFT => moveLeft()
case KeyEvent.VK_RIGHT => moveRight()
case _ =>
}
repaint()
}
})

override def paintComponent(gg: Graphics): Unit = {
super.paintComponent(gg)
val g = gg.asInstanceOf[Graphics2D]
g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON)
drawGrid(g)
}

private def drawGrid(g: Graphics2D): Unit = {
val (gridColor, emptyColor, startColor) = (new Color(0xBBADA0), new Color(0xCDC1B4), new Color(0xFFEBCD))

if (game == State.running) {
g.setColor(gridColor)
g.fillRoundRect(200, 100, 499, 499, 15, 15)
for (
r <- 0 until side;
c <- 0 until side
) if (Option(tiles(r)(c)).isEmpty) {
g.setColor(emptyColor)
g.fillRoundRect(215 + c * 121, 115 + r * 121, 106, 106, 7, 7)
}
else drawTile(g, r, c)
} else {
g.setColor(startColor)
g.fillRoundRect(215, 115, 469, 469, 7, 7)
g.setColor(gridColor.darker)
g.setFont(new Font("SansSerif", Font.BOLD, 128))
g.drawString("2048", 310, 270)
g.setFont(new Font("SansSerif", Font.BOLD, 20))
if (game == State.won) g.drawString("you made it!", 390, 350)
else if (game == State.over) g.drawString("game over", 400, 350)
g.setColor(gridColor)
g.drawString("click to start a new game", 330, 470)
g.drawString("(use arrow keys to move tiles)", 310, 530)
}
}

private def drawTile(g: Graphics2D, r: Int, c: Int): Unit = {
val value = tiles(r)(c).value
g.setColor(colorTable((math.log(value) / math.log(2)).toInt + 1))
g.fillRoundRect(215 + c * 121, 115 + r * 121, 106, 106, 7, 7)
g.setColor(if (value < 128) colorTable.head else colorTable(1))
val (s, fm) = (value.toString, g.getFontMetrics)
val asc = fm.getAscent
val (x, y) = (215 + c * 121 + (106 - fm.stringWidth(s)) / 2, 115 + r * 121 + (asc + (106 - (asc + fm.getDescent)) / 2))
g.drawString(s, x, y)
}

private def moveUp(checkingAvailableMoves: Boolean = false) = move(-1, 0, checkingAvailableMoves)

private def moveDown(checkingAvailableMoves: Boolean = false) = move(1, 0, checkingAvailableMoves)

private def moveLeft(checkingAvailableMoves: Boolean = false) = move(0, -1, checkingAvailableMoves)

private def moveRight(checkingAvailableMoves: Boolean = false) = move(0, 1, checkingAvailableMoves)

private def clearMerged(): Unit = for (row <- tiles; tile <- row) if (Option(tile).isDefined) tile.setMerged()

private def movesAvailable() = moveUp(true) || moveDown(true) || moveLeft(true) || moveRight(true)

def move(yIncr: Int, xIncr: Int, checkingAvailableMoves: Boolean): Boolean = {
var moved = false
for (i <- 0 until side;
j <- 0 until side) if (Option(tiles(i)(j)).isDefined) {
var (r, c) = (i, j)
var (nextR, nextC, breek) = (i + yIncr, j + xIncr, false)
while ((nextR >= 0 && nextR < side && nextC >= 0 && nextC < side) && !breek) {
val (next, curr) = (tiles(nextR)(nextC), tiles(r)(c))

if (Option(next).isEmpty)
if (checkingAvailableMoves) return true
else {
tiles(nextR)(nextC) = curr
tiles(r)(c) = null
r = nextR
c = nextC
nextR += yIncr
nextC += xIncr
moved = true
}
else if (next.canMergeWith(curr)) {
if (checkingAvailableMoves) return true
highest = math.max(next.mergeWith(curr), highest)
tiles(r)(c) = null
breek = true
moved = true
} else breek = true
}
}
updateState(moved)
moved
}

private def updateState(moved: Boolean): Unit = {
if (moved && highest < target) {
clearMerged()
addRandomTile()
if (!movesAvailable) game = State.over
}
else if (highest == target) game = State.won
}

private def startGame(): Unit = if (game ne State.running) {
highest = 0
game = State.running
tiles = Array.ofDim[Tile](side, side)
addRandomTile()
addRandomTile()
}

private def addRandomTile(): Unit = {
var pos = rand.nextInt(side * side)
var (row, col) = (0, 0)
do {
pos = (pos + 1) % (side * side)
row = pos / side
col = pos % side
} while (Option(tiles(row)(col)).isDefined)
tiles(row)(col) = Tile(if (rand.nextInt(10) == 0) 4 else 2)
}
}
}

完整代码

后面可以考虑加上AI,尝试基于规则或强化学习的方法